
Alkaline.CompileResult = function(stdOut, stdErr, stdWarn) {
	if (Alkaline.init)
		Alkaline.init();
	
	if (Alkaline.CompileResult.prototype._setupClass)
		Alkaline.CompileResult.prototype._setupClass();
	
	this.stdOut = stdOut; this.stdErr = stdErr; this.stdWarn = stdWarn;
	
	this.inputSource = new Alkaline.InputSource(stdOut, stdErr, stdWarn);
	
	this.messages = [];
	this.warnings = [];
	this.errors = [];
	
	this.AST = null;
	this.nextStage = 1;
	this.exception = null;
	this.treeAdaptor = null;	// the tree adaptor is available after stage1
	this.treeBuilder = null;	// available after stage1

	this.modules = [];
	this.modules.byName = {};
	this.outputModule = new Alkaline.Modules.Printer(this);

	var oThis = this;
	this.getLocation = function() {
		return oThis.inputSource.getLocation.apply(oThis.inputSource, arguments);
	}
	this._error_bound = function(msg) {
		oThis._error(msg);
	}
	this._warn_bound = function(msg) {
		oThis._warn(msg);
	}
}

Alkaline.CompileResult.prototype.addModule = function(moduleName) {
	if (!Alkaline.Modules[moduleName.toLowerCase()]) {
		this.stdWarn("Module " + moduleName + " does not exist or is not loaded. Ignoring.");
		return false;
	}
	var module = new Alkaline.Modules[moduleName.toLowerCase()](this);
	moduleName = module.moduleName;	// correct lower/upper case
	if (module.requiredModules && module.requiredModules.length)
		for (var i = 0; i < module.requiredModules.length; i++) {
			var req = Alkaline.Modules[module.requiredModules[i].name];
			var found = null;
			for (var j = 0; j < this.modules.length; j++)
				if (this.modules[j] instanceof req) {
					found = this.modules[j];
					break;
				}
			if (!found) {
				if (!this.addModule(module.requiredModules[i].name))
					// unable to load dependency
					return false;
				found = this.modules[this.modules.length-1];
			}
			// <- set up module options if needed
		}
	if (module.moduleType == Alkaline.MODULE_TYPES.OUTPUT)
		this.outputModule = module;
	else {
		this.modules.push(module);
		this.modules.byName[moduleName] = module;
	}
	return true;
}

Alkaline.CompileResult.prototype.getLocation = function(line, pos) {
	throw new Error('CompileResult->getLocation is an abstract method! This should have been overwritten.');
}

Alkaline.CompileResult.prototype.getModuleWhichSupportsArgument = function(argName) {
	for (var i = 0; i < this.modules.length; i++)
		if (this.modules[i].supportsArgument(argName))
			return this.modules[i];
	if (this.outputModule.supportsArgument(argName))
		return this.outputModule;
	return null;
}

Alkaline.CompileResult.prototype.supportsArgument = function(argName) {
	return this.getModuleWhichSupportsArgument(argName) !== null;
}

Alkaline.CompileResult.prototype.getArgumentArity = function(argName) {
	var module = this.getModuleWhichSupportsArgument(argName);
	if (module)
		return module.getArgumentArity(argName);
	return -1;
}

Alkaline.CompileResult.prototype.setArgument = function(argName, options) {
	var module = this.getModuleWhichSupportsArgument(argName);
	if (module)
		return module.setArgument(argName, options);
}

Alkaline.CompileResult.prototype.stage1 = function() {
	var oThis = this;
	var cstream = new org.antlr.runtime.ANTLRStringStream(this.inputSource.input);
	var lexer = new AlkalineLexer(cstream);
	lexer.getLocation = this.getLocation;
	lexer.emitErrorMessage = this._error_bound;
	lexer.emitWarningMessage = this._warn_bound;
	var tstream = new org.antlr.runtime.CommonTokenStream(lexer);
	var parser = new AlkalineParser(tstream);
	parser.getLocation = this.getLocation;
	parser.emitErrorMessage = this._error_bound;
	parser.emitWarningMessage = this._warn_bound;
	var result;
	try {
		this.AST = parser.prog().tree;
		this.nextStage = 2;
		result = !this.errors.length;
	} catch(e) {
		result = false;
		this.stdErr('CRASHED when parsing.');
		this.errors.push('CRASH');
		if (!(e instanceof org.antlr.runtime.RecognitionException || e instanceof org.antlr.runtime.tree.RewriteCardinalityException))
			// return unhandled exception
			this.exception = e;
	}
	if (result) {
		this.treeAdaptor = this.AST.treeAdaptor = parser.adaptor;
		this.treeBuilder = new Alkaline.TreeBuilder(this, AlkalineParser);
		this._markFunctionDeclarations();
		this._simplifyAST();
	}
	return result;
}

Alkaline.CompileResult.prototype.stage2 = function() {
	for (var i = 0; i < this.modules.length; i++) {
		this.modules[i].run(this.AST);
		if (this.errors.length)
			return false;
	}
	this.nextStage = 3;
	if (this.modules.length)
		this._simplifyAST();
	return true;
}

Alkaline.CompileResult.prototype.stage3 = function() {
	this.outputModule.run(this.AST);
	this.output = this.outputModule.outputText;
	this.nextStage = 4;
	return true;
}

Alkaline.CompileResult.prototype._markFunctionDeclarations = function() {
	var TOKENS = AlkalineParser;
	var items = this.AST.children;
	if (items)	// if the source is empty there are no children
		for (var i = 0; i < items.length; i++)
			if (items[i].token.type === TOKENS.Function)
				items[i].isFunctionDeclaration = true;
}

/**
 *	Removes redundant parent-child relations in the tree (e.g. if an EXPR contains just an ITEM, replaces the EXPR with the ITEM)
 */
Alkaline.CompileResult.prototype._simplifyAST = function() {
	var hash = Alkaline.CompileResult.prototype._simplifyAST.hash;
	var root = this.AST;
	
	var isFirstPass = false;
	if (!this._simplifiedAST) {
		this._simplifiedAST = true;
		isFirstPass = true;
	}
	
	var TOKENS = Alkaline.JSInfo.TOKEN;
	var parser = new AlkalineParser;
	var getOpPriority = function(ast) {
		return parser.getOpPriority(ast.token.text || ast.token.getText());
	}
	
	var shouldRemove = function(root) {
		if (root.children && root.children.length === 1)
			if (root.token.type === TOKENS.EXPR)
				return true;
			else if (root.parent) {
				if (hash[root.token.type] && (hash[root.token.type][root.children[0].token.type] || hash[root.token.type]['*']))
					return true;
			}
		return false;
	}
	
	while (root) {
		while (shouldRemove(root)) {
			
			if (root.parent)
				root.parent.children[root.childIndex] = root.children[0];
			root.children[0].childIndex = root.childIndex;
			root.children[0].parent = root.parent;
			root = root.children[0];
		}

		// make sure all the tokens have the <text> property
		if (isFirstPass && root.token)
			root.token.getText(); // sometimes the text property is not created for some nodes until it is requested
		
		// continue to the next node
		if (root.children && root.children.length) {
			root = root.children[0];
		} else {
			// bottom-up processing of trees is done in the method _doneWithTree (one tree at a time)
			while (root.parent && root.parent.children.length === root.childIndex + 1) {
				root = this._doneWithTree(root);
				root = root.parent;
			}
			root = this._doneWithTree(root);
			root = root.parent ? root.parent.children[root.childIndex+1] : null;
		}
	}
};

Alkaline.CompileResult.prototype._doneWithTree = function(ast) {
	var TOKENS = Alkaline.JSInfo.TOKEN;
	var ret = ast;
	switch (ast.token.type) {
		case TOKENS.BLOCK:
			if (ast.children && ast.children.length === 1 && ast.parent && [ TOKENS.Do, TOKENS.For, TOKENS.If, TOKENS.While, TOKENS.With ].contains(ast.parent.token.type) &&
				ast.children[0].token.type !== TOKENS.Comment) {
				ast.parent.setChild(ast.childIndex, ast.children[0]);
				ret = ast.children[0];
			}
			break;
		
		case TOKENS.Operator:
			if (ast.children && ast.children.length === 2 && ast.children[1].token.text === ast.token.text && Alkaline.JSInfo.associativeOperators.contains(ast.token.text)) {
				// (&& a (&& b c)) -> (&& (&& a b) c)
				// (&& a (&& (&& b c) d)) -> (&& (&& (&& a b) c) d)
				var newast = ast.children[1];
				ast.parent.replaceChildren(ast.childIndex, ast.childIndex, newast);
				var bottom = newast.children[0];
				while (bottom.token.text === ast.token.text)
					bottom = bottom.children[0];
				var bottomParent = bottom.parent;
				ast.replaceChildren(1, 1, bottom);
				bottomParent.replaceChildren(0, 0, ast);
				ret = newast;
			}
			break;
		
		case TOKENS.Number:
			ast.token.text = Alkaline.JSInfo.shortestNumberRepresentation(ast.token.text);
			break;
	}
	return ret !== ast ? this._doneWithTree(ret) : ast;
}

Alkaline.CompileResult.prototype._setupClass = function() {
	var TOKENS = AlkalineParser;
	var hash = Alkaline.CompileResult.prototype._simplifyAST.hash = [];
	
	var add = function(parent, child) {
		if (!(parent in TOKENS))
			throw new Error('Token "' + parent + '" does not exist');
		if (!(child in TOKENS) && child != '*')
			throw new Error('Token "' + child + '" does not exist');
		parent = TOKENS[parent];
		if (child != '*')
			child = TOKENS[child];
		if (!hash[parent])
			hash[parent] = {};
		hash[parent][child] = true;
	}

	add('EXPR', 'EXPR');
	//add('EXPR', 'ITEM');	// unless inside a SELECT
	//add('Operator', 'EXPR');	// depends on the precedency of the operator inside the EXPR (if it is an op)
	//add('EXPR', 'Operator');
	add('ITEM', '*');
	add('EXPR', 'Function');

	delete Alkaline.CompileResult.prototype._setupClass;
};

Alkaline.CompileResult.prototype._error = function(msg) {
	this.errors.push(msg);
	this.stdErr(msg);
}

Alkaline.CompileResult.prototype._warn = function(msg) {
	this.warnings.push(msg);
	this.stdWarn(msg);
}

Alkaline.CompileResult.prototype._msg = function(msg) {
	this.messages.push(msg);
	this.stdOut(msg);
}

Alkaline.CompileResult.prototype.run = function(){
	if (!this.inputSource.files.length) {
		this.stdErr('No input file(s) specified.');
		return;
	}
	this.stdOut('Stage 1 - parsing (' + this.inputSource.input.length + ' characters)');
	var d = new Date().valueOf();
	this.stage1();
	this.stdOut('Parsed in ' + Math.round((new Date().valueOf()-d)/1000) + ' seconds.');
	if (this.exception || this.errors.length)
		return false;
	
	this.stdOut('Stage 2 - modules (' + this.modules.pluck('moduleName').join(', ') + ')');
	this.stage2();
	if (this.exception || this.errors.length)
		return false;
	
	this.stdOut('Stage 3 - output (module ' + this.outputModule.moduleName + ')');
	this.stage3();
	this.stdOut('Done in ' + Math.round((new Date().valueOf()-d)/1000) + ' seconds.');
	return typeof this.output === 'string';
}
